require 'rake'
require 'stringio'

class Spinner
  VERSION = "0.3.0".freeze

  attr_reader :queue

  def initialize(*tasks)
    @queue = tasks
    @width = 0
    @chars = %w{ | / - \\ }
  end

  # Injects a new task into the queue.
  def task(title=nil, task_name=nil, &block)
    # Handle no task being supplied.
    return @queue.map(&:title) unless block_given? || task_name

    # Set default title.
    title = 'Executing' unless title

    # Tasks can be given a block to evaluate or
    # a rake task name to invoke.
    if block_given?
      task_block = block
    elsif task_name
      task_block = Rake::Task[task_name]
    end

    # Inject the task into the queue.
    @queue << [ title, task_block ]

    # Amend the output width if necessary.
    if @width < title.length
      @width = title.length
    end

    # Return the new list of tasks.
    @queue.map(&:first)
  end

  # Starts executing the queued tasks.
  def spin!
    # Handle no tasks in the queue.
    return unless @queue.any?

    # Mark the current time in order to calculate the total duration.
    start_time = Time.now

    # Pluralize the number of tasks in the queue.
    task_counter = "#{@queue.size} task" << (@queue.size == 1 ? '' : 's')

    # Update the print width.
    @width = (@width + (@queue.size.to_s.length + 2) * 2) + 1

    # Execute each task in sequence.
    @queue.each_with_index do |task, i|
      run_task(task, i+1)
    end

    # Reset this spinner instance so that it can be reused.
    reset!

    # Mark the completion time and calculate the duration.
    end_time = Time.now
    time_taken = distance_of_time_in_words(start_time, end_time)

    # Print the completion message.
    print("Done! #{task_counter} completed in #{time_taken} :-)\n")
  end

  private

  # Outputs to the console.
  def print(*args)
    STDOUT.print(*args)
  end

  # Clears the current printed output.
  def clear
    print("\r")
    print(" ".ljust(@width + 5))
    print("\r")
  end

  # Reset this spinner instance to defaults.
  def reset!
    @width = 0
    @queue = []
  end

  # Executes a single task.
  def run_task(item, counter)
    # Extract the title & task block.
    title, task = *item

    # Print the task counter and title.
    print("#{counter}/#{queue.size}: #{title}".ljust(@width, '.') + '... ')

    # Begin a new thread to update the printed output while
    # the task runs.
    t = Thread.new {
      # Suppress $stdout during the task's execution.
      $stdout = StringIO.new
      if task.respond_to?(:invoke)
        task.invoke
      else
        task.call
      end
    }

    # Run the spinner for the duration of the task,
    # then clear the output.
    spin while t.alive?
    t.join
    clear
  end

  # Update the position of the spinner.
  def spin
    print(@chars[0])          # Print the next character...
    sleep(0.1)                # ...wait 100ms...
    print("\b")               # ...move the cursor back by one...
    @chars.push(@chars.shift) # ...rotate the characters array.
  end

  # Simplified extraction from ActionView.
  def distance_of_time_in_words(from_time, to_time)
    distance_in_minutes = ((to_time - from_time) / 60.0).round
    distance_in_seconds = (to_time - from_time).round

    case distance_in_minutes
      when 0..1
        case distance_in_seconds
          when 0..4   then "less than 5 seconds"
          when 5..9   then "less than 10 seconds"
          when 10..19 then "less than 20 seconds"
          when 20..39 then "about half a minute"
          when 40..59 then "less than one minute"
          else             "one minute"
        end

      when 2...45  then "about #{distance_in_minutes} minutes"
      when 45...90 then "about an hour"
      else              "over an hour"
    end
  end
end
